#include <retro.h>
#include "retro.library.h"

using namespace Retro;
using namespace Retro::Data;
using namespace SPLib;
using namespace SPLib::Windows;
using namespace SPLib::Geom;

#pragma region Declarations
PTCHAR resFolders[] = {
  _T("map"),
  _T("texture"),
  _T("sprite"),
  _T("audio"),
  _T("script"),
};

PTCHAR resNames[] = {
  _T("Sprite Maps"),
  _T("Grid Maps"),
  _T("Textures"),
  _T("Sprites"),
  _T("Audio"),
  _T("Scripts")
};

PTCHAR resExt[] = {
  _T("smp"),
  _T("gmp"),
  _T("tex"),
  _T("spr"),
  _T("aud"),
  _T("scr")
};
#pragma endregion

DWORD gBytesIO;

#define Retro_ReadStruct(f, s) \
  if (!ReadFile(f, &s, sizeof(s), &gBytesIO, NULL) || \
  gBytesIO != sizeof(s)) { \
  return FALSE; }

#define Retro_ReadBuffer(f, p, t, l) \
  p = new t[l / sizeof(t)]; \
  if (!IO::ReadBuffer(f, p, l)) { \
  delete[] p; p = NULL; return FALSE; }

#define Retro_WriteStruct(f, s) \
  if (!WriteFile(f, &s, sizeof(s), &gBytesIO, NULL) || \
  gBytesIO != sizeof(s)) { \
  return FALSE; }

#define Retro_WriteBuffer(f, p, l) \
  if (!IO::WriteBuffer(f, p, l)) \
  return FALSE;

Data::Resource* Retro_LoadResource(HANDLE iFile)
{
  Resource::Header  wHeader;
  Resource*         wResource;

  Retro_ReadStruct(iFile, wHeader);

  switch (wHeader.type)
  {
  case Data::ResourceGridMap:
    wResource = new GridMap();
    break;
  case Data::ResourceSpriteMap:
    wResource = new SpriteMap();
    break;
  case Data::ResourceTexture:
    wResource = new Texture();
    break;
  case Data::ResourceScript:
    wResource = new Script();
    break;
  default:
    return NULL;
  }

  memcpy(&wResource->headerResource, &wHeader, sizeof(wHeader));
  wResource->headerResource.name[RETRO_MAXNAME - 1] = _T('\0');

  wResource->setInitialized();

  if (!wResource->open(iFile))
    wResource->setCorrupted();

  return wResource;
}

PCTSTR Retro::Data::GetResourceFolder(ResourceType type)
{
  switch (type)
  {
  case ResourceGridMap:
  case ResourceSpriteMap:
    return resFolders[FolderMap - FolderFirst];
  case ResourceTexture:
    return resFolders[FolderTexture - FolderFirst];
  case ResourceAudio:
    return resFolders[FolderAudio - FolderFirst];
  case ResourceSprite:
    return resFolders[FolderSprite - FolderFirst];
  case ResourceScript:
    return resFolders[FolderScript - FolderFirst];
  }

  return NULL;
}

PCTSTR Retro::Data::GetResourceName(Data::ResourceType type)
{
  if (type >= ResourceFirst && type <= ResourceLast)
    return resNames[type - ResourceFirst];

  return NULL;
}

PCTSTR Retro::Data::GetResourceExt(Data::ResourceType type)
{
  if (type >= ResourceFirst && type <= ResourceLast)
    return resExt[type - ResourceFirst];

  return NULL;
}

INT RetroProject::getNextResourceID(Data::ResourceType type) const
{
  BOOL wIDFound;
  INT wID = 1;

  do
  {
    wIDFound = FALSE;

    for (UINT i = 0; i < mResources.getLength(); ++i)
    {
      if (mResources[i]->getType() == type &&
        mResources[i]->getID() == wID)
      {
        ++wID;
        wIDFound = TRUE;
        break;
      }
    }
  }
  while (wIDFound);

  return wID;
}

Data::Resource* RetroProject::createResource(Data::ResourceType type)
{
  Resource* res;

  switch (type)
  {
  case Data::ResourceGridMap:
    res = new Data::GridMap();
    break;
  case Data::ResourceSpriteMap:
    res = new Data::SpriteMap();
    break;
  case Data::ResourceTexture:
    res = new Data::Texture();
    break;
  case Data::ResourceScript:
    res = new Data::Script();
    break;
  default:
    return NULL;
  }

  res->headerResource.type = type;
  res->headerResource.id = getNextResourceID(type);
  res->setInitialized();
  res->setUnsaved();

  return res;
}

Data::Resource* RetroProject::findResource(
  Data::ResourceType type,
  INT id
  ) const
{
  for (UINT i = 0; i < mResources.getLength(); ++i)
  {
    if (mResources[i]->getType() == type &&
      mResources[i]->getID() == id)
    {
      return mResources[i];
    }
  }

  return NULL;
}

Data::Resource* RetroProject::findResource(
  Data::ResourceType type,
  PCTSTR name
  ) const
{
  for (UINT i = 0; i < mResources.getLength(); ++i)
  {
    if (mResources[i]->getType() == type &&
      _tcscmp(mResources[i]->getName(), name) == 0)
    {
      return mResources[i];
    }
  }

  return NULL;
}

RetroProject::RetroProject()
{
  memset(&mHeader, 0, sizeof(mHeader));

  mOpened = FALSE;
}

VOID RetroProject::init()
{
  close();
  memset(&mHeader, 0, sizeof(mHeader));

  mOpened = TRUE;

  for (int i = (int)ResourceFirst; i < (int)ResourceLast; ++i)
    CreateDirectory(resFolders[i], NULL);
}

BOOL RetroProject::open(PCTSTR iFilename)
{
  HANDLE wFile = CreateFile(
    iFilename,
    GENERIC_READ,
    0,
    NULL,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    NULL);

  if (wFile == INVALID_HANDLE_VALUE)
    return FALSE;

  close();

  mOpened = IO::ReadBuffer(wFile, &mHeader, sizeof(mHeader));

  CloseHandle(wFile);

  if (!mOpened)
    return FALSE;

  TCHAR wCurrDir[MAX_PATH];

  _tcscpy(wCurrDir, iFilename);

  PathRemoveFileSpec(wCurrDir);
  SetCurrentDirectory(wCurrDir);

  openResources();

  return TRUE;
}

VOID RetroProject::close()
{
  for (UINT i = 0; i < mResources.getLength(); ++i)
  {
    mResources[i]->free();
    delete mResources[i];
  }

  mResources.clear();
  mOpened = FALSE;
}

BOOL RetroProject::save(PCTSTR iFilename)
{
  if (!mOpened)
    return FALSE;

  HANDLE wFile = CreateFile(
    iFilename,
    GENERIC_WRITE,
    0,
    NULL,
    CREATE_ALWAYS,
    FILE_ATTRIBUTE_NORMAL,
    NULL);

  if (wFile == INVALID_HANDLE_VALUE)
    return FALSE;

  BOOL wResult = wResult = IO::WriteBuffer(wFile, &mHeader, sizeof(mHeader));

  CloseHandle(wFile);

  return wResult;
}

VOID RetroProject::openResources()
{
  WIN32_FIND_DATA wFindData;
  HANDLE wFindFile;

  for (int i = (int)FolderFirst; i <= (int)FolderLast; ++i)
  {
    CreateDirectory(resFolders[i - FolderFirst], NULL);

    if (SetCurrentDirectory(resFolders[i - FolderFirst]))
    {
      BOOL state = (wFindFile = FindFirstFile(RETRO_RESOURCEFIND, &wFindData))
        != INVALID_HANDLE_VALUE;

      while (state)
      {
        HANDLE wFile = CreateFile(
          wFindData.cFileName,
          GENERIC_READ,
          0,
          NULL,
          OPEN_EXISTING,
          FILE_ATTRIBUTE_NORMAL,
          NULL);

        if (wFile != INVALID_HANDLE_VALUE)
        {
          Data::Resource* wResource = Retro_LoadResource(wFile);

          CloseHandle(wFile);

          if (wResource)
            addResource(wResource);
        }

        state = FindNextFile(wFindFile, &wFindData);
      }

      SetCurrentDirectory(_T("..\\"));
      FindClose(wFindFile);
    }
  }
}

VOID RetroProject::saveResources()
{
  for (UINT i = 0; i < mResources.getLength(); ++i)
  {
    PCTSTR folder = GetResourceFolder(mResources[i]->getType());

    CreateDirectory(folder, NULL);
    SetCurrentDirectory(folder);

    if (mResources[i]->isDeleted())
    {
      mResources[i]->deleteFile();
      delete mResources[i];
      mResources.remove(i--);
    }
    else
    {
      mResources[i]->save();
    }

    SetCurrentDirectory(_T("..\\"));
  }
}

VOID RetroProject::uninitDeletedResources()
{
  for (UINT i = 0; i < mResources.getLength(); ++i)
  {
    if (mResources[i]->isDeleted())
      mResources[i]->setUninitialized();
  }
}

BOOL RetroProject::hasUnsavedResources() const
{
  for (UINT i = 0; i < mResources.getLength(); ++i)
  {
    if (mResources[i]->isUnsaved())
      return TRUE;
  }

  return FALSE;
}

BOOL Resource::save()
{
  TCHAR wFilename[MAX_PATH];

  if (isCorrupted())
    return FALSE;

  _stprintf(
    wFilename,
    RETRO_RESOURCENAMEFORMAT,
    getID(),
    GetResourceExt(getType()));

  HANDLE wFile = CreateFile(
    wFilename,
    GENERIC_WRITE,
    0,
    NULL,
    CREATE_ALWAYS,
    FILE_ATTRIBUTE_NORMAL,
    NULL);

  if (wFile != INVALID_HANDLE_VALUE)
  {
    if (save(wFile))
      setSaved();

    CloseHandle(wFile);
  }

  return isSaved();
}

BOOL Resource::save(HANDLE iFile)
{
  Retro_WriteStruct(iFile, headerResource);

  return TRUE;
}

VOID Resource::deleteFile()
{
  TCHAR filename[MAX_PATH];

  _stprintf(
    filename,
    RETRO_RESOURCENAMEFORMAT,
    getID(),
    GetResourceExt(getType()));

  DeleteFile(filename);
}

VOID Texture::setPath(PCTSTR value)
{
  SetCurrentDirectory(GetResourceFolder(getType()));

  DeleteFile(headerTexture.path);

  _tcscpy(headerTexture.path, getName());
  _tcscat(headerTexture.path, PathFindExtension(value));

  CopyFile(value, headerTexture.path, FALSE);

  if (Retro::IsEditorMode())
  {
    bitmap = new Gdip::Bitmap(getPath());

    width   = bitmap->GetWidth();
    height  = bitmap->GetHeight();
  }

  SetCurrentDirectory(_T("..\\"));
}

VOID Texture::setTextureType(TextureType value)
{
  SafeDelete(tileset);

  if ((headerTexture.type = value) == TextureTileset &&
    getDataTileset().tileSize > 0 && width && height)
  {
    SIZE_T wBufferSize = width * height / getDataTileset().tileSize;

    tileset = new Tileset();
    tileset->data = new TileData[wBufferSize];
  }
}

BOOL Texture::save(HANDLE iFile)
{
  if (!Resource::save(iFile))
    return FALSE;

  Retro_WriteStruct(iFile, headerTexture);

  if (tileset)
  {
    TCHAR mFilename[MAX_PATH];

    getTilesetFilename(mFilename);

    HANDLE mTilesFile = CreateFile(
      mFilename,
      GENERIC_WRITE,
      0,
      NULL,
      CREATE_ALWAYS,
      FILE_ATTRIBUTE_NORMAL,
      NULL);

    if (mTilesFile != INVALID_HANDLE_VALUE)
    {
      SIZE_T wBufferSize =
        width / getDataTileset().tileSize *
        height / getDataTileset().tileSize;

      IO::WriteBuffer(
        mTilesFile,
        tileset->data,
        wBufferSize * sizeof(TileData));

      CloseHandle(mTilesFile);
    }
  }

  return TRUE;
}

BOOL Texture::open(HANDLE iFile)
{
  Retro_ReadStruct(iFile, headerTexture);

  bitmap = new Gdip::Bitmap(getPath());

  width   = bitmap->GetWidth();
  height  = bitmap->GetHeight();

  if (!Retro::IsEditorMode())
    SafeDelete(bitmap);

  if (headerTexture.type == TextureTileset && width && height)
  {
    TCHAR mFilename[MAX_PATH];

    SIZE_T wBufferSize =
      width / getDataTileset().tileSize *
      height / getDataTileset().tileSize;

    tileset = new Tileset();
    tileset->data = new TileData[wBufferSize];

    memset(tileset->data, 0, wBufferSize * sizeof(TileData));

    getTilesetFilename(mFilename);

    HANDLE mTilesFile = CreateFile(
      mFilename,
      GENERIC_READ,
      0,
      NULL,
      OPEN_EXISTING,
      FILE_ATTRIBUTE_NORMAL,
      NULL);

    if (mTilesFile != INVALID_HANDLE_VALUE)
    {
      IO::ReadBuffer(mTilesFile, tileset->data, wBufferSize * sizeof(TileData));

      CloseHandle(mTilesFile);
    }
  }

  return TRUE;
}

VOID Texture::deleteFile()
{
  TCHAR mFilename[MAX_PATH];

  getTilesetFilename(mFilename);

  DeleteFile(mFilename);
  DeleteFile(headerTexture.path);

  Resource::deleteFile();
}

VOID Texture::free()
{
  SafeDelete(tileset);
  SafeDelete(bitmap);
}

BOOL Map::open(HANDLE iFile)
{
  Retro_ReadStruct(iFile, headerMap);

  headerMap.width  = max(1, headerMap.width);
  headerMap.height = max(1, headerMap.height);
  headerMap.layers = max(1, min(headerMap.layers, RETRO_MAP_MAXLAYERS));

  return TRUE;
}

BOOL Map::save(HANDLE iFile)
{
  if (!Resource::save(iFile))
    return FALSE;

  Retro_WriteStruct(iFile, headerMap);

  return TRUE;
}

VOID Map::setSize(INT cx, INT cy, INT layers)
{
  headerMap.width     = cx;
  headerMap.height    = cy;
  headerMap.layers    = layers;
}

BOOL GridMap::open(HANDLE iFile)
{
  if (!Map::open(iFile))
    return FALSE;

  headerMap.width  = min(headerMap.width,  RETRO_GRIDMAP_MAXWIDTH);
  headerMap.height = min(headerMap.height, RETRO_GRIDMAP_MAXHEIGHT);

  pGrid = new GridCell*[getLayers()];

  for (int i = 0; i < getLayers(); ++i)
    pGrid[i] = NULL;

  for (int i = 0; i < getLayers(); ++i)
  {
    Retro_ReadBuffer(
      iFile,
      pGrid[i],
      GridCell,
      sizeof(GridCell) * getWidth() * getHeight());
  }

  Retro_ReadStruct(iFile, headerGrid);

  return TRUE;
}

BOOL GridMap::save(HANDLE iFile)
{
  if (!Map::save(iFile))
    return FALSE;

  for (int i = 0; i < getLayers(); ++i)
  {
    Retro_WriteBuffer(iFile, pGrid[i],
      sizeof(GridCell) * getWidth() * getHeight());
  }

  Retro_WriteStruct(iFile, headerGrid);

  return TRUE;
}

VOID GridMap::free()
{
  if (getGrid())
  {
    for (int i = 0; i < getLayers(); ++i)
      delete[] getGrid()[i];

    delete[] getGrid();
  }
}

VOID GridMap::setSize(INT cx, INT cy, INT layers)
{
  GridCell** newGrid  = new GridCell*[layers];
  INT maxW            = min(getWidth(), cx);
  INT maxH            = min(getHeight(), cy);
  INT maxL            = min(getLayers(), layers);

  for (int l = 0; l < layers; ++l)
  {
    newGrid[l] = new GridCell[cx * cy];
    memset(newGrid[l], -1, sizeof(GridCell) * cx * cy);

    if (l < maxL && pGrid)
    {
      for (int y = 0; y < maxH; ++y)
      {
        memcpy(
          &newGrid[l][y * cx],
          &pGrid[l][y * getWidth()],
          sizeof(GridCell) * maxW);
      }
    }
  }

  if (pGrid)
  {
    for (int l = 0; l < getLayers(); ++l)
      delete[] pGrid[l];
    delete[] pGrid;
  }

  pGrid = newGrid;
  Map::setSize(cx, cy, layers);
}

BOOL SpriteMap::open(HANDLE iFile)
{
  if (!Map::open(iFile))
    return FALSE;

  headerMap.width  = min(headerMap.width,  RETRO_SPRITEMAP_MAXWIDTH);
  headerMap.height = min(headerMap.height, RETRO_SPRITEMAP_MAXHEIGHT);

  Retro_ReadStruct(iFile, headerSprite);

  buffer = new SpriteBuffer[headerMap.layers];

  for (int i = 0; i < headerMap.layers; ++i)
    memset(&buffer[i], 0, sizeof(buffer[i]));

  for (int i = 0; i < headerMap.layers; ++i)
  {
    Retro_ReadStruct(iFile, buffer[i].header);

    if (buffer[i].header.length > 0)
    {
      Retro_ReadBuffer(
        iFile,
        buffer[i].sprites,
        SpriteData,
        sizeof(SpriteData) * buffer[i].header.length);
    }
  }

  return TRUE;
}

BOOL SpriteMap::save(HANDLE iFile)
{
  if (!Map::save(iFile))
    return FALSE;

  Retro_WriteStruct(iFile, headerSprite);

  if (buffer)
  {
    for (int i = 0; i < headerMap.layers; ++i)
    {
      Retro_WriteStruct(iFile, buffer[i].header);

      if (buffer[i].header.length)
      {
        Retro_WriteBuffer(
          iFile,
          buffer[i].sprites,
          sizeof(SpriteData) * buffer[i].header.length);
      }
    }
  }

  return TRUE;
}

VOID SpriteMap::free()
{
  if (buffer)
  {
    for (int i = 0; i < getLayers(); ++i)
      delete[] buffer[i].sprites;

    delete[] buffer;
  }
}

VOID SpriteMap::setSize(INT cx, INT cy, INT layers)
{
  SpriteBuffer* newBuffer = layers ? new SpriteBuffer[layers] : NULL;
  INT maxLayer            = min(getLayers(), layers);

  for (int l = 0; l < layers; ++l)
  {
    if (l < maxLayer && buffer)
    {
      memcpy(&newBuffer[l].header, &buffer[l].header,
        sizeof(newBuffer[l]));

      if (buffer[l].sprites)
      {
        newBuffer[l].sprites = new SpriteData[buffer[l].header.length];

        memcpy(newBuffer[l].sprites, buffer[l].sprites,
          sizeof(SpriteData) * buffer[l].header.length);
      }
      else
      {
        newBuffer[l].sprites = NULL;
      }
    }
    else
    {
      memset(&newBuffer[l], 0, sizeof(newBuffer[l]));
    }
  }

  if (buffer)
  {
    for (int i = 0; i < getLayers(); ++i)
      delete[] buffer[i].sprites;

    delete[] buffer;
  }

  buffer = newBuffer;

  Map::setSize(cx, cy, layers);
}

BOOL Script::save(HANDLE iFile)
{
  if (!Resource::save(iFile))
    return FALSE;

  Retro_WriteStruct(iFile, headerScript);

  if (buffer)
  {
    SIZE_T wBufferLength = sizeof(CHAR) * (strlen(buffer) + 1);

    Retro_WriteBuffer(iFile, buffer, wBufferLength);
  }

  return TRUE;
}

BOOL Script::open(HANDLE iFile)
{
  LARGE_INTEGER wSize;

  GetFileSizeEx(iFile, &wSize);
  wSize.QuadPart -= sizeof(headerResource) + sizeof(headerScript);

  Retro_ReadStruct(iFile, headerScript);
  Retro_ReadBuffer(iFile, buffer, CHAR, (DWORD)wSize.QuadPart);

  return TRUE;
}

VOID Script::free()
{
  delete[] buffer;
}
